/**
* touchNote
* @fileOverview 划线备注
* @author kevin Lv
* @email donggongai@126.com
* @version 1.0.0
* @date 2021-7-5
*/
;(function($){
	/**
	* @author kevin Lv
	* @constructor touchNote
	* @description 划线备注
	* @see The <a href="#">kevin</a>
	* @example
	* 	对页面内容划线，并对划线部分进行备注
	*	$("#div").touchNote();
	*   带参数的用法 function中event为事件，root为根元素，ele调用元素，options为配置参数。
	*	$("#div").touchNote({
			hlClsName:'highligh_my',
			mouseover:function(event, root, ele, options){
				alert(ele.val());
			}
		});
	*   
	* @since version 1.0
	*/
	var winHeight = $(window).height();
	// 改变窗口大小
	$(window).on("resize.touchnote", function(e){
		winHeight = $(window).height();
		console.log('winHeight:'+winHeight);
	});
	touchNote = {
		root:document.body,
		SplitType : { none : 'none', head : 'head', tail : 'tail', both : 'both'},
		OPERATE_Type : { ADD : 'add', UPDATE : 'update'},
		SelectedNodeType : { text : 'text', span : 'span' },
		TOUCHNOTE_INDEX : 'tn-index',
		DATA_ID : 'highlight-id',
		DATA_ID_EXTRA : 'highlight-id-extra',
		TMP_DATA_ID : 'tmp-highlight-id',
		DATA_SPLIT_TYPE : 'highlight-split-type',
		CAMEL_DATA_ID : 'highlightId',
		CAMEL_DATA_ID_EXTRA : 'highlightIdExtra',
		CAMEL_TMP_DATA_ID : 'tmpHighlightId',
		CAMEL_DATA_SPLIT_TYPE : 'highlightSplitType',
		ROOT_IDX : -2,
		UNKNOWN_IDX : -1,
		ID_DIVISION : ';',	
			// 获取
		getSelection : function(root){
			root = root || this.root;
			var selection = null;
			if (window.getSelection) {
				selection = window.getSelection();
			} else if (document.selection) {
				selection = document.selection;
			}
			var rangeOri = window.getSelection().getRangeAt(0);
			
			var range = rangeOri.cloneRange();//复制rang对象

			
			var start = {node: range.startContainer, offset: range.startOffset};
			var end = {node: range.endContainer, offset: range.endOffset};
			
			// 获取选区内的文本节点
			
			var nodes = getSelectedNodes(root, start, end);
			
			var text = this.getSelectionText();
			var htmltext = getSelectHtmlText(nodes);
			
			return new Selection(selection, range, nodes, text, htmltext);

		},
		getHighlightSource : function(root, selection, parSelectors) {
			root = root || this.root;
			var range = selection.range;
			var start = {node: range.startContainer, offset: range.startOffset};
			var end = {node: range.endContainer, offset: range.endOffset};
			var range = selection.range;
			var text = selection.text;
			var highlightSource = this.serialize(root, start, end, text, "tn-"+new Date().getTime(),parSelectors);
			return highlightSource;
		},		
		serializeSelection : function(root, selection, parSelectors) {			
			var highlightSource = this.getHighlightSource(root, selection, parSelectors);
			return this.serialize2Str(highlightSource);;
		},
		// 序列化来 root: Document | HTMLElement, hooks: HookMap
		serialize : function(root, start, end, text, id, parSelectors) {
			root = root || this.root;
			var startMeta = getDomMeta(start.node, start.offset, root, parSelectors);
			var endMeta = getDomMeta(end.node, end.offset, root, parSelectors);
			var highlightSource = new HighlightSource(startMeta, endMeta, text, id);
			return highlightSource;
		},
		serialize2Str : function(highlightSource) {
			var cloneJsonObj = $.extend(true, {}, highlightSource);//对象克隆
			//delete cloneJsonObj['create'];
			//delete cloneJsonObj['deSerialize'];
			var dataStr=JSON.stringify(cloneJsonObj);
			return dataStr;
		},
		deSerializefromStr : function(root, datastr) {
			root = root || this.root;			
			var highlightSource = $.extend(true, new HighlightSource(), jQuery.parseJSON(datastr));//对象克隆					
			var range = highlightSource.deSerialize(root);
		},
		deSerializefromhs : function(root, highlightSource) {	
			root = root || this.root;
			var range = highlightSource.deSerialize(root);
		},
		// 判读节点是否高亮包裹 node: HTMLElement : boolean
		isHighlightWrapNode : function(node){
			return !!node.dataset && !!node.dataset[touchNote.CAMEL_DATA_ID];
		},
		// 获取选中的文本
		getSelectionText : function(){
			var text = '';
			if (window.getSelection) {
				text = window.getSelection().toString();
			} else if (document.selection && document.selection.type != "Control") {
				text = document.selection.createRange().text;
			}
			return text;
		},
		// 获取选中的文本 高亮包裹
		// selected{SelectedNode}, range{HighlightRange}, className{string[] | string}, wrapTag{string} return HTMLElement
		wrapHighlight : function(selected, range, className, wrapTag) {
			var parentNode = selected.node.parentNode;
			var prevNode = selected.node.previousSibling;
			var nextNode = selected.node.nextSibling;

			var wrapNode; //: HTMLElement;

			/* 在处理时，将需要包裹的各个文本片段（Text Node）分为三类情况：

			1、完全未被高亮包裹，则直接包裹该部分。
			2、属于被高亮包裹过的文本节点的一部分，则使用.splitText()将其拆分。
			3、是一段完全被高亮包裹的文本段，只更新id */

			// 没有包裹高亮则进行包裹
			if (!touchNote.isHighlightWrapNode(parentNode)) {
				wrapNode = wrapNewNode(selected, range, className, wrapTag);
			}
			// 部分高亮包裹，拆分
			else if (touchNote.isHighlightWrapNode(parentNode) && (!isEmptyNode(prevNode) || !isEmptyNode(nextNode))) {
				wrapNode = wrapPartialNode(selected, range, className, wrapTag);
			}
			// 剩下的是已经高亮包裹的，值增加extra id
			else {
				wrapNode = wrapOverlapNode(selected, range, className);
			}

			return wrapNode;
		},
		// 移除指定id的高亮，如果对应id的高亮不存在，则返回false
		removeHighlight: function(rootEle, id, callback) {
			console.log(id)
			// whether extra ids contains the target id id; 或者;id 分号可以没有
			var reg = new RegExp('(' + id + touchNote.ID_DIVISION + '|' + touchNote.ID_DIVISION + '?' + id + '$)'); // data中highlightIdExtra的正则

			var wrapTag = rootEle.getOptions('hltagName') //'span'; //高亮包裹元素
			var spans = rootEle.find(wrapTag+'[data-'+touchNote.DATA_ID+']'); // 获取所有高亮元素
			

			// 移除的元素
			var toRemoves = []; 
			// 更新主id和extraid的元素
			var idToUpdates = []; 
			// 更新extra id的元素
			var extraToUpdates = []; 

			$.each(spans, function(i, n){
				var spanId = n.dataset[touchNote.CAMEL_DATA_ID]; // highlightId
				var spanExtraIds = n.dataset[touchNote.CAMEL_DATA_ID_EXTRA]; //highlightIdExtra
				

				// id相等并且没有extraid -> 移除
				if (spanId === id && !spanExtraIds) { 
					toRemoves.push(n);
				}
				// id相等并且但是存在extraid -> 更新主id和extraid
				else if (spanId === id) {
					console.log(spanExtraIds)
					idToUpdates.push(n);
				}
				// id不相等但是extraid包含 -> 更新extra id
				else if (spanId !== id && reg.test(spanExtraIds)) {
					extraToUpdates.push(n);
				}
			});
			console.log(toRemoves)
			console.log(idToUpdates)
			console.log(extraToUpdates)
			// 移除
			$.each(toRemoves, function(i, n){
				var parent = n.parentNode;
				var fr = document.createDocumentFragment();
				
				$.each(n.childNodes, function(i, c){
					fr.appendChild(c.cloneNode(false));
				});				

				var prev = n.previousSibling;
				var next = n.nextSibling;

				parent.replaceChild(fr, n);
				// 合并相邻的文本节点
				normalizeSiblingText(prev, true);
				normalizeSiblingText(next, false);
				//hooks.Remove.UpdateNodes.call(id, n, 'remove');
			});			

			$.each(idToUpdates, function(i, n){
				var dataset = n.dataset;
				var ids = dataset[touchNote.CAMEL_DATA_ID_EXTRA].split(touchNote.ID_DIVISION);
				var newId = ids.shift(); // 把数组的第一个元素从其中删除,并返回第一个元素的值

				// 寻找对应extraId对应的元素
				var overlapSpanEle = rootEle.find(wrapTag+'[data-'+touchNote.DATA_ID+'="'+newId+'"]');

				if (overlapSpanEle.length) {
					// 移除样式
					removeAllClass(n);
					// 添加样式 retain the class list of the overlapped wrapper which associated with "extra id"
					var clsList = overlapSpanEle[0].classList;
					console.log(clsList)
					for(var index=0;index<clsList.length;index++){
						$(n).addClass(clsList.item(index));						
					}
					
					var nid = overlapSpanEle.data("nid"); // nid更新
					var username = overlapSpanEle.data("username");
					var createtime = overlapSpanEle.data("createtime");
					
					$(n).data("nid", nid);
					$(n).attr("data-nid", nid);
					$(n).data("username", username);
					$(n).attr("data-username", username);
					$(n).data("createtime", createtime);
					$(n).attr("data-createtime", createtime); 
				} else { // 从备注区找
					var noteItemClsName = rootEle.getOptions('noteItemClsName');
					var noteEle = $("."+noteItemClsName+"[data-keyid='"+newId+"']");
					var nid = noteEle.data("nid");
					
					var username = noteEle.data("username");
					var createtime = noteEle.data("createtime");
					$(n).data("nid", nid);
					$(n).attr("data-nid", nid);
					$(n).data("username", username);
					$(n).attr("data-username", username);
					$(n).data("createtime", createtime);
					$(n).attr("data-createtime", createtime); 
				}			
				
				var idsv = ids.join(touchNote.ID_DIVISION); // highlightIdExtra，更新extraid
				$(n).data(touchNote.CAMEL_DATA_ID, newId);
				$(n).attr("data-"+touchNote.DATA_ID, newId); // highlightId 更新主id
				$(n).data(touchNote.CAMEL_DATA_ID_EXTRA, idsv);
				$(n).attr("data-"+touchNote.DATA_ID_EXTRA, idsv);	
			});
			// 去掉extraId中的id
			$.each(extraToUpdates, function(i, n){
				var extraIds = n.dataset[touchNote.CAMEL_DATA_ID_EXTRA];

				var extraIdv = extraIds.replace(reg, '');
				// n.dataset[touchNote.CAMEL_DATA_ID_EXTRA] = extraIdv; // highlightIdExtra
				$(n).data(touchNote.CAMEL_DATA_ID_EXTRA, extraIdv);
				$(n).attr("data-"+touchNote.DATA_ID_EXTRA, extraIdv); // highlightId 更新主id
				
			});
			if(callback && $.isFunction(callback)){
				callback(toRemoves, idToUpdates, extraToUpdates);
			}
			return toRemoves.length + idToUpdates.length + extraToUpdates.length !== 0;
		},
		// 移除所有的高亮
		removeAllHighlight : function(rootEle, callback) {
			var spans = getHighlightsByRoot(rootEle, rootEle.getOptions('hltagName')); //获取根元素下的所有高亮元素,return jQueryObj

			spans.each(function(i, n){
				var parent = n.parentNode;
				var fr = document.createDocumentFragment();

				$.each(n.childNodes, function(i, c){ //循环子节点
					fr.appendChild(c.cloneNode(false)); //浅克隆=标签里的元素，属性、事件没有复制。
				});
				parent.replaceChild(fr, n); // 将高亮用新生成的元素替换
			});
			if(callback && $.isFunction(callback)){
				callback(spans);
			}			
		}
	}
	// 选区对象
	function Selection(selection, range, selectedNodes, text, htmltext) {
		this.selection = selection;
		this.range = {
			startContainer: range.startContainer, 
			startOffset: range.startOffset,
			endContainer: range.endContainer, 
			endOffset: range.endOffset,
			domRect : range.getBoundingClientRect()
		}		
		
		this.selectedNodes = selectedNodes;
		this.text = text;
		this.htmltext = htmltext;
	}
	// 源码对象
	function HighlightSource(startMeta, endMeta, text, id) {
		this.startMeta = startMeta; // DomMeta
		this.endMeta=endMeta; // DomMeta
		this.text = text; // string
		this.id = id; // string		
	}
	// 反序列化
	HighlightSource.prototype.deSerialize = function(root) {
		var { start, end } = queryElementNode(this, root);
		if(!start && !end){ //不存在则返回null
			return null;
		}
		var startInfo = getTextChildByOffset(start, this.startMeta.textOffset);
		var endInfo = getTextChildByOffset(end, this.endMeta.textOffset);
		var range = new HighlightRange(startInfo, endInfo, this.text, this.id, true);
		return range;
	}
	// 高亮处理 highlighter{Highlighter}
	HighlightSource.prototype.highlight = function(root, highlighter) {
		var range = this.deSerialize(root);
		var selectedNodes = getSelectedNodes(root, range.start, range.end);
	
		var nodes = [];
		$.each(selectedNodes, function(i, n){
			var node = touchNote.wrapHighlight(n, range, highlighter.className, highlighter.tagName);
			nodes.push(node);
		});
		return nodes;
	}
	// 高亮对象
	function Highlighter(className, tagName) {
		this.className = className; // DomMeta
		this.tagName=tagName; // DomMeta
	}
	// 选中范围对象，用于与原生range转换
	function HighlightRange(start, end, text, id) {
		if ((start && start.node.nodeType !== 3) || (end && end.node.nodeType !== 3)) {
			console.log("类型错误！")
		}
		if(start){
			this.start = formatDomNode(start); // DomMeta
		}
		if(end){
			this.end = formatDomNode(end); // DomMeta
		}
		
		this.text = text; // string
		this.id = id; // string
	}
	// 获取根元素下的所有高亮元素,return jQueryObj
	function getHighlightsByRoot(rootEles, wrapTag) {		

		var wraps;

		rootEles.each(function(i, n){
			var list = $(n).find(wrapTag+'[data-'+touchNote.DATA_ID+']');
			if (!wraps) {
				wraps = list;
			} else {
				wraps = wraps.add(list);
			}			
		});

		return wraps;
	}
	/**
	 * 合并相邻的文本节点
	 * .normalize() API 在ie11下有bug
	 */
	function normalizeSiblingText(s, isNext) {
		if (!s || s.nodeType !== 3) {
			return;
		}

		var sibling = isNext ? s.nextSibling : s.previousSibling;

		if (sibling.nodeType !== 3) {
			return;
		}

		var text = sibling.nodeValue;

		s.nodeValue = isNext ? s.nodeValue + text : text + s.nodeValue;
		sibling.parentNode.removeChild(sibling);
	}
	// 获取配置
	$.fn.getOptions = function(optionName){
		var eles = $(this); //表单
		if(optionName){
			return eles.data("options")[optionName];
		}
		return eles.data("options");
	}
	// 获取选中选区对象
	$.fn.getSelection = function(){
		var selection = $(this).data('selection'); //data中取
		return selection;
	}
	// 根据选中对象增加高亮处理，并增加到备注区
	$.fn.addHighlighter = function(remarks){
		var eles = $(this); //表单
		var options = eles.getOptions();
		var noteClassName = options.hlClsName; // 批注样式
		var root = options.root;
		
		// 移除临时高亮,防止干扰
		removeTmpHighlight(options.hltmpClsName);		
		
		var selection = eles.getSelection(); //获取selection
		
		// 获取批注，并处理赋值
		var highlighter =  new Highlighter(noteClassName, options.hltagName);
		// 根据数据处理显示，高亮，并增加悬停提示
		
		var highlightSource = touchNote.getHighlightSource(root, selection, options.parSelectors);	

		if(highlightSource.startMeta){
			var noteClassInitClsName = options.hlIntClsName; //高亮已经初始化的样式
			// 高亮,可能存在多个高亮元素
			var nodes = highlightSource.highlight(root, highlighter);
			// 处理提示内容
			var data = {'username':options.username,'createtime':new Date()};
			data.keyid = highlightSource.id;
			data.keycode = highlightSource;
			data.remarks = remarks; //备注信息
			
			// 添加到备注区
			var i = $(options.noteContainerSelector).find("."+options.noteItemClsName).length;
			var noteEle = options.addNoteEle(data, i, null, $(options.noteContainerSelector), root, eles, options);
			// 更新备注总数 count, datas, root, options
			options.updateNotesCount(i+1, null, root, eles, options);
			// 处理高亮
			$.each(nodes, function(i, n){
				var nodeEle = $(n);
				var hid = nodeEle.data(touchNote.CAMEL_DATA_ID);
				var tipsClass = 'codetips_'+hid;
				var tipsContentClass = 'content_'+tipsClass;				
				
				if(!nodeEle.hasClass(noteClassInitClsName)){
					
					// 调用处理方法
					eles.triggerHandler("dealCodeNoteEle", [nodeEle, data, tipsClass, tipsContentClass]); //添加样式
				}
				// 高亮处理完成后的处理方法，可再自己的实现中扩展
				options.highlightNoteAfter(nodeEle, noteEle, data, tipsClass, tipsContentClass, eles, options);
					
			});
			
			
			return nodes;
		}
		return null;
		
	}
		
	$.fn.touchNote = function(options){
		debug("选择器["+this.selector+"]调用自定义插件 touchNote，调用元素数 "+this.length+" 个[jQuery "+this.jquery+"]");
		/**
		* @description {Json} 设置参数选项
		* @field
		*/
		var options = $.extend({}, $.fn.touchNote.defaults, options);
		var noteClassName = options.hlClsName; // 批注样式
		var noteClassInitClsName = options.hlIntClsName; //高亮已经初始化的样式
		
		var hldatas = options.hldatas;
		var root = options.root;
		touchNote.root = root;
		/**
		* @description 处理函数
		*/
		var eles = $(this); //表单
		eles.each(function(i, n){
			var $this = $(this);
			$this.attr('data-'+touchNote.TOUCHNOTE_INDEX, i);
			$this.data('options', options);
			
			// 处理父节点
			$this.off("dealCodeNoteEle").on("dealCodeNoteEle", function(event, ele, data, tipsClass, tipsContentClass){
				
				var remarks = data.remarks; // 内容
				var keycode = data.keycode;
				var username = data.username;
				var createtime = data.createtime;
				ele.addClass(noteClassInitClsName).addClass(tipsClass).data('obj',data); //添加样式				
				addDatas(ele, data);
				
				// 高亮选区鼠标移入移出的处理
				ele.off("mouseenter.touchnote").on("mouseenter.touchnote", function(event){
					var e = event || window.event;
					options.highlightNoteMouseover[0](e, root, ele, options);				
				},);
				ele.off("mouseleave.touchnote").on("mouseleave.touchnote", function(event){
					var e = event || window.event;
					options.highlightNoteMouseover[1](e, root, ele, options);
				});
			});
			
			// 获取批注，并处理赋值
			var highlighter =  new Highlighter(noteClassName, options.hltagName);
			// 根据数据处理显示，高亮，并增加悬停提示
			$.each(hldatas, function(i, n){
				var data = n;
				var nid = data.nid;
				var keycode = data.keycode;
				
				// 添加到备注区
				var noteEle = options.addNoteEle(data, i, hldatas, $(options.noteContainerSelector), root, $this, options);
				
				try {	
					var highlightSource = $.extend(true, new HighlightSource(), keycode);//对象克隆					
					
					if(highlightSource.startMeta){
						// 获取高亮元素
						highlightSource.highlight(root, highlighter);
						
						// 处理提示内容
						var hid = keycode.id;
						var tipsClass = 'codetips_'+hid;
						var tipsContentClass = 'content_'+tipsClass;
						// 提示元素创建
						var codeNodeEles = $('.'+noteClassName+"[data-"+touchNote.DATA_ID+"='"+hid+"']");
						if(codeNodeEles.length>0){
							codeNodeEles.each(function(){
								var nodeEle = $(this);
								if(!nodeEle.hasClass(noteClassInitClsName)){
									$this.triggerHandler("dealCodeNoteEle", [nodeEle, data, tipsClass, tipsContentClass]); //添加样式
								}								
								// 高亮处理完成后的处理方法，可再自己的实现中扩展
								options.highlightNoteAfter(nodeEle, noteEle, data, tipsClass, tipsContentClass, $this, options);								
							});
						}
						
					}
										
				} catch(e) { console.log(e); }			
				
				
			});
			// 更新备注总数 count, datas, root, options
			options.updateNotesCount(hldatas.length, hldatas, root, $this, options);
			// 备注区更新数目
			$(options.noteContainerSelector).off("refreshCount").on("refreshCount", function(){ //删除					
				// 更新备注总数 count, datas, root, options
				options.refreshNotesCount($(this), root, $this, options);					
			});			
			
			// 监听释放鼠标按钮事件
			$this.off("mouseup.touchnote").on("mouseup.touchnote", function(event){
				var e = event || window.event;
				options.mouseup(e, root, $this, options);
			});

			// 监听释放鼠标划过事件
			$this.off("mouseover.touchnote").on("mouseover.touchnote", function(event){
				var e = event || window.event;
				options.mouseover(e, root, $this, options);				
			});
			
		});
		// 取消无效的高亮显示
		$(document).off("click.hl.touchnote").on("click.hl.touchnote", function(event){
			var targetE = $(event.target);
			
			$("."+noteClassName).not('.'+noteClassInitClsName).removeClass(noteClassName);
			$(".note-dialog").hide(); //弹出框隐藏

		});

		
	};
	//私有方法
	/**
	* @description 输出选中对象的个数到控制台
	* @param {msg} String 输出内容
	*/
	function debug(msg) {
		if (window.console && window.console.log){
			window.console.log('touchnote.js log: ' + msg);
		}
	}
	
	/**
	 * 如果开始节点结束节点相同，不需要遍历树.
	 */
	 // startNode: Text, startOffset: number, endOffset: number
	var getNodesIfSameStartEnd = function(startNode, startOffset, endOffset) {
		var element = startNode;

		while (element) {
			element = element.parentNode;
		}

		startNode.splitText(startOffset);
		var passedNode = startNode.nextSibling; // Text
		passedNode.splitText(endOffset - startOffset);
		return [{
			node: passedNode,
			type: touchNote.SelectedNodeType.text,
			splitType: touchNote.SplitType.both
		}];
	};
	
	// root: Document | HTMLElement, start: DomNode, end: DomNode
	// 获取选中节点 返回示例：{node: node, type: touchNote.SelectedNodeType.text, splitType: touchNote.SplitType.head}
	var getSelectedNodes = function (root, start, end){
		var startNode = start.node;
		var endNode = end.node;
		var startOffset = start.offset;
		var endOffset = end.offset;

		// 如果开始节点和结束节点相同 拆分当前节点
		if (startNode === endNode && startNode instanceof Text) {			
			return getNodesIfSameStartEnd(startNode, startOffset, endOffset);
		}

		var nodeStack = [root];
		var selectedNodes = [];

		var withinSelectedRange = false;
		var curNode = null;
		// 遍历dom树
		while ((curNode = nodeStack.pop())) {
			
			var children = curNode.childNodes;

			for (var i = children.length - 1; i >= 0; i--) {
				nodeStack.push(children[i]);
			}

			// 只选择Text节点，即文本节点
			if (curNode === startNode) {
				if (curNode.nodeType === 3) {
					curNode.splitText(startOffset);

					var node = curNode.nextSibling;
					if(!node.nodeValue){
						console.log(node)
					}
					//if(node.nodeValue){ // 有内容
						selectedNodes.push({
							node: node,
							type: touchNote.SelectedNodeType.text,
							splitType: touchNote.SplitType.head
						});
					//}
					
				}
				// meet the start-node (begin to traverse)
				withinSelectedRange = true;
			} else if (curNode === endNode) {
				if (curNode.nodeType === 3) {
					var node = curNode;

					node.splitText(endOffset);
					if(!node.nodeValue){
						console.log(node)
					}
					if(node.nodeValue){ // 有内容
						selectedNodes.push({
							node: node,
							type: touchNote.SelectedNodeType.text,
							splitType: touchNote.SplitType.tail
						});
					}
					
				}
				// meet the end-node
				break;
			}
			// 处理两个选区中间的文本节点
			else if (withinSelectedRange && curNode.nodeType === 3) {
				
				if(!curNode.nodeValue){
					console.log(curNode)
				}
				if(curNode.nodeValue){ // 有内容
					selectedNodes.push({
						node: curNode,
						type: touchNote.SelectedNodeType.text,
						splitType: touchNote.SplitType.none
					});
				}
				
			}
		}

		return selectedNodes;
	};

	// 获取选中文本
	var getSelectText = function(nodes){
		var text = "";
		if(nodes){
			for(var i=0;i<nodes.length;i++){
				text += nodes[i].node.textContent;
			}
		}
		return text;
	}
	// 获取选中的html文本(暂未实现)
	var getSelectHtmlText = function(nodes){
		var text = "";
		return text;
	}
	
	// 获取前一个文本偏移量 root: Node, text: Node
	function getTextPreOffset(root, text) {
		var nodeStack = [root];
		var curNode = null;
		var offset = 0;
		while (curNode = nodeStack.pop()) {
			var children = curNode.childNodes;
			for (let i = children.length - 1; i >= 0; i--) {
				nodeStack.push(children[i]);
			}

			if (curNode.nodeType === 3 && curNode !== text) {
				offset += curNode.textContent.length;
			}
			else if (curNode.nodeType === 3) {
				break;
			}
		}

		return offset;
	}
	// 是否是预期的元素
	var isExcepted = function(e, exceptSelectors){
		if(exceptSelectors){
			return $(e).is(exceptSelectors);
		}
		return true;
	} 
	/**
	 * 寻找未高亮包裹的原始dom父类
	 */
	 // node: HTMLElement | Text: HTMLElement
	var getOriginParent = function(node, parSelectors){
		if (node instanceof HTMLElement && (!node.dataset || ( !node.dataset[touchNote.CAMEL_DATA_ID] && !node.dataset[touchNote.CAMEL_DTMP_DATA_ID] )) && isExcepted(node, parSelectors)) {
			return node;
		}

		var originParent = node.parentNode;		
		
		// ?.安全输出,es6写法，不兼容 originParent?.dataset originParent && originParent.dataset
		// 如果存在高亮(或临时选中)属性则继续找父类，即高亮(或临时选中)不作为父类
		while ( (originParent && ( originParent.dataset[touchNote.CAMEL_DATA_ID] || originParent.dataset[touchNote.CAMEL_DTMP_DATA_ID] ) ) || !isExcepted(originParent, parSelectors)) {
			originParent = originParent.parentNode;
		}
		return originParent;
	};
	// 计算根节点下node所在的tag的下标 node{Node}, root{Document | HTMLElement} :return number 
	var countGlobalNodeIndex = function(node, root) {
		var tagName = node.tagName;
		var list = root.getElementsByTagName(tagName);

		for (let i = 0; i < list.length; i++) {
			if (node === list[i]) {
				return i;
			}
		}

		return touchNote.UNKNOWN_IDX;
	};
	// 获取dom源数据
	// node: HTMLElement | Text, offset: number, root: Document | HTMLElement
	var getDomMeta = function (node, offset, root, parSelectors){
		var originParent = getOriginParent(node, parSelectors);
		var index = originParent === root ? touchNote.ROOT_IDX : countGlobalNodeIndex(originParent, root);
		
		var preNodeOffset = getTextPreOffset(originParent, node);
		var tagName = originParent.tagName;

		return {
			parentTagName: tagName,
			parentIndex: index,
			textOffset: preNodeOffset + offset,
		};
	};
	
	// 通过偏移量获取子类文本节点和相对偏移量 parentNode: Node, offset: number): DomNode
	function getTextChildByOffset(parent, offset) {
		var nodeStack = [parent];
		var curNode = null;
		var curOffset = 0;
		var startOffset = 0;
		while (curNode = nodeStack.pop()) {
			var children = curNode.childNodes;
			for (let i = children.length - 1; i >= 0; i--) {
				nodeStack.push(children[i]);
			}
			if (curNode.nodeType === 3) {
				startOffset = offset - curOffset;
				curOffset += curNode.textContent.length;
				if (curOffset >= offset) {
					break;
				}
			}
		}
		if (!curNode) {
			curNode = parent;
		}
		return {node: curNode, offset: startOffset};
	}

	/**
	 * 通过HighlightSource对象的meta信息获取开始结束父类元素
	 *
	 * @param {HighlightSource} 高亮源对象
	 * @param {HTMLElement | Document} root根节点, 默认document.body
	 * @return {Object} { start, end }
	 */
	 // hs: HighlightSource, root: Document | HTMLElement): { start: Node; end: Node }
	var queryElementNode = function(hs, root) {		
		var	start =
			hs.startMeta.parentIndex === touchNote.ROOT_IDX
				? root
				: root.getElementsByTagName(hs.startMeta.parentTagName)[hs.startMeta.parentIndex];

		var end = hs.endMeta.parentIndex === touchNote.ROOT_IDX
				? root
				: root.getElementsByTagName(hs.endMeta.parentTagName)[hs.endMeta.parentIndex];
		return { start, end };
	};

	// 格式化dom节点对象，n: DomNode): DomNode
	var formatDomNode = function(n) {
		if (
			// Text
			n.node.nodeType === 3 ||
			// CDATASection
			n.node.nodeType === 4 ||
			// Comment
			n.node.nodeType === 8
		) {
			return n;
		}

		return {
			node: n.node.childNodes[n.offset],
			offset: 0,
		};
	};	
	

	// el: HTMLElement, className?: string[] | string): HTMLElement
	var addClass = function(el, className) {
		var classNames = Array.isArray(className) ? className : [className];
	
		$.each(classNames, function(i, n) {
			$(el).addClass(n);
		});
	
		return el;
	};
	/**
	 * support IE 10
	 */
	 // arr: T[]): T[]
	var unique = function(arr) {
		var res = [];

		for (var i=0;i<arr.length;i++) {
			var el = arr[i];
			if (res.indexOf(el) === -1) {
				res.push(el);
			}
		}

		return res;
	};

	/**
	 * 部分包裹，拆分节点 Split and wrapper each one.
	 */
	 // selected: SelectedNode, range: HighlightRange, className: string[] | string, wrapTag: string,: HTMLElement
	var wrapPartialNode = function(selected, range, className, wrapTag) {
		var wrapNode = document.createElement(wrapTag);

		var parentNode = selected.node.parentNode;
		var prevNode = selected.node.previousSibling;
		var nextNode = selected.node.nextSibling;
		var fragment = document.createDocumentFragment();
		var parentId = parentNode.dataset[touchNote.CAMEL_DATA_ID];
		var parentExtraId = parentNode.dataset[touchNote.CAMEL_DATA_ID_EXTRA];
		var extraInfo = parentExtraId ? parentId + touchNote.ID_DIVISION + parentExtraId : parentId;

		wrapNode.setAttribute('data-'+touchNote.DATA_ID, range.id);
		wrapNode.setAttribute('data-'+touchNote.DATA_ID_EXTRA, extraInfo);
		wrapNode.appendChild(selected.node.cloneNode(false));

		var headSplit = false;
		var tailSplit = false;
		var splitType;

		if (prevNode) {
			var cloneNode = parentNode.cloneNode(false);

			cloneNode.textContent = prevNode.textContent;
			fragment.appendChild(cloneNode);
			headSplit = true;
		}

		var classNameList = [];

		if (Array.isArray(className)) {
			classNameList.push(...className);
		} else {
			classNameList.push(className);
		}

		addClass(wrapNode, unique(classNameList));
		fragment.appendChild(wrapNode);

		if (nextNode) {
			var cloneNode = parentNode.cloneNode(false);

			cloneNode.textContent = nextNode.textContent;
			fragment.appendChild(cloneNode);
			tailSplit = true;
		}

		if (headSplit && tailSplit) {
			splitType = touchNote.SplitType.both;
		} else if (headSplit) {
			splitType = touchNote.SplitType.head;
		} else if (tailSplit) {
			splitType = touchNote.SplitType.tail;
		} else {
			splitType = touchNote.SplitType.none;
		}

		wrapNode.setAttribute('data-'+touchNote.DATA_SPLIT_TYPE, splitType);
		parentNode.parentNode.replaceChild(fragment, parentNode);

		return wrapNode;
	};
	/**
	 * 完全未被包裹，则直接包裹该部分 Wrap a common wrapper.
	 */
	 // selected: SelectedNode, range: HighlightRange, className: string[] | string, wrapTag: string): HTMLElement
	var wrapNewNode = function(selected, range, className, wrapTag) {
		var wrapNode = document.createElement(wrapTag);

		addClass(wrapNode, className);

		wrapNode.appendChild(selected.node.cloneNode(false));
		selected.node.parentNode.replaceChild(wrapNode, selected.node);

		wrapNode.setAttribute('data-'+touchNote.DATA_ID, range.id);
		wrapNode.setAttribute('data-'+touchNote.DATA_SPLIT_TYPE, selected.splitType);
		wrapNode.setAttribute('data-'+touchNote.DATA_ID_EXTRA, '');

		return wrapNode;
	};
	// 移除所有的样式
	var removeAllClass = function(el) {
		el.className = '';
	};
	// 是否是空节点
	var isEmptyNode = function(n){return !n || !n.textContent;};
	// node{HTMLElement}, root{RootElement}:return string
	var getHighlightId = function(node, root) {
		node = findAncestorWrapperInRoot($node, $root);

		if (!node) {
			return '';
    }
	// node{HTMLElement}, root{RootElement}:return HTMLElement
	var findAncestorWrapperInRoot = function(node, root) {
		var isInsideRoot = false;
		var wrapper = null;

		while (node) {
			if (touchNote.isHighlightWrapNode(node)) {
				wrapper = node;
			}

			if (node === root) {
				isInsideRoot = true;
				break;
			}

			node = node.parentNode;
		}

		return isInsideRoot ? wrapper : null;
	};

    return node.dataset[touchNote.CAMEL_DATA_ID];
};

	/**
	 * 完全被包裹的文本段，只更新idJust update id info (no wrapper updated).
	 */
	 // selected{SelectedNode}, range{HighlightRange}, className{string[] | string)} :return HTMLElement
	var wrapOverlapNode = function(selected, range, className) {
		var parentNode = selected.node.parentNode;
		var wrapNode = parentNode;

		removeAllClass(wrapNode);
		addClass(wrapNode, className);

		var dataset = parentNode.dataset;
		var formerId = dataset[touchNote.CAMEL_DATA_ID];

		dataset[touchNote.CAMEL_DATA_ID] = range.id;
		dataset[touchNote.CAMEL_DATA_ID_EXTRA] = dataset[touchNote.CAMEL_DATA_ID_EXTRA]
			? formerId + touchNote.ID_DIVISION + dataset[touchNote.CAMEL_DATA_ID_EXTRA]
			: formerId;

		return wrapNode;
	};
	// 
	var walkToRenderCommentBlock = function(range, lastRightOffset, currentAnnotationId, lastAnnotationByIdNode) {
		var currentRightOffset = range.domRect.right; //距离右侧位置

		if (lastRightOffset > currentRightOffset) {
			// 找到了插入点
			doRenderCommentBlock(range, currentAnnotationId, lastAnnotationByIdNode)
		} else {
			// 继续向下一个字符寻找
			if (range.endOffset < range.endContainer.textContent.length - 1) {
				// 如果当前 range 还没找到头，那就继续下一个
				var currentRange = document.createRange();
				currentRange.setStart(range.endContainer, range.endOffset);

				// 如果结束节点类型是 Text, Comment, or CDATASection 之一, 那么 endOffset 指的是从结束节点算起字符的偏移量
				// 对于其他 Node 类型节点， endOffset 是指从结束结点开始算起子节点的偏移量。

				try {
					// 存在 range.endOffset + 1 不存在的情况 (比如空标签)，这时候就用下一个节点
					currentRange.setEnd(range.endContainer, range.endOffset + 1);
				} catch (e) {
					currentRange.setStart(getNextSiblingNode(range.endContainer), 0);
					currentRange.setEnd(getNextSiblingNode(range.endContainer), 1);
				}

				return walkToRenderCommentBlock(currentRange, currentRightOffset, currentAnnotationId, lastAnnotationByIdNode);
			} else {
				// 如果当前 range 到头了，还没有找到，则找下一个 nodeText
				var nextNode = getNextTextNode(range.endContainer);
				if (nextNode) {
					var currentRange = document.createRange();
					currentRange.setStart(nextNode, 0);
					currentRange.setEnd(nextNode, 1);
					return walkToRenderCommentBlock(currentRange, currentRightOffset, currentAnnotationId, lastAnnotationByIdNode);
				} else {
					// 找到了当前段的最后面，在段后加
					doRenderCommentBlock(null, currentAnnotationId, lastAnnotationByIdNode);
				}
			}
		}	
	}	
	
	var highlightNoteAfter = function(hlele, noteEle, data, tipsClass, tipsContentClass, containerE, options){
		// 点击弹出修改窗口
		var url = containerE.data('url-upd');
		hlele.off('click.touchnote').on('click.touchnote', function(event){
			var datas = hlele.data();
			var params = {keycode:JSON.stringify(datas.keycode), 
					keyid:datas.keycode.id,
					startTag:datas.keycode.startMeta.parentTagName,
					startTagIndex:datas.keycode.startMeta.parentIndex,
					startTextOffset:datas.keycode.startMeta.textOffset,
					endTag:datas.keycode.endMeta.parentTagName,
					endTagIndex:datas.keycode.endMeta.parentIndex,
					endTextOffset:datas.keycode.endMeta.textOffset,
					text:datas.keycode.text, 
					htmltext:datas.keycode.text,
					remarks:datas.content};
			// 打开弹出框			
			options.openDialog(hlele, touchNote.OPERATE_Type.UPDATE, url, params, '修改备注', containerE, options);
			return false; //防止冒泡
		});
	}
	function warapTmpHighlight(nodes, hltmpClsName, id){
		$.each(nodes, function(i, n){
			var tmpEle = $(n.node);
			
			tmpEle.wrap('<span class="'+hltmpClsName+'" data-'+touchNote.TMP_DATA_ID+'="'+id+'"></span>');
		});
	}
	function removeTmpHighlight(hltmpClsName){
		$('.'+hltmpClsName).each(function(i, n){
			
			$(n.childNodes[0]).unwrap(); //取第一个子类剥离父类，下面的方法不是用使因为没有更新dom树，parentNode或parent()依然能查到
			// 等同于 $(n).replaceWith( n.childNodes );
			
			//var prev = n.previousSibling;
			//var next = n.nextSibling;
			
			// 方法1：jquery替换 方法2：先添加后移除
			//var tmpEle = $(n);
			//var my_html = tmpEle.html();
			//tmpEle.replaceWith(my_html);
			//tmpEle.before(my_html);
			//tmpEle.remove();			
			
			// 方法3、使用原始js
			//var parent = n.parentNode;
			//var fr = document.createDocumentFragment();

			//$.each(n.childNodes, function(i, c){ //循环子节点
				
				//fr.appendChild(c.cloneNode(false)); //浅克隆=标签里的元素，属性、事件没有复制。
			//});
			//parent.replaceChild(fr, n); // 将高亮用新生成的元素替换
			
			// 合并相邻的文本节点
			//normalizeSiblingText(prev, true);
			//normalizeSiblingText(next, false);			
		});
	}
	// 释放鼠标处理函数
	function mouseUp(event, root, ele, options) {
		
		// 移除临时高亮
		removeTmpHighlight(options.hltmpClsName);
		
		var text = touchNote.getSelectionText();
		var noteToolEle = $(".note-tool");
		if (text) {
			var selection = touchNote.getSelection(root); //获取selection eles.getSelection();	
			
			htmltext = selection.text; //html
			var serializeStr = touchNote.serializeSelection(root, selection, options.parSelectors);
			var range = selection.range;			
			
			var target = event.target;
			var targetE = $(target);
			if(targetE.length){
				// 执行高亮
				if (noteToolEle.length==0) {
					
					var html = options.tooltopsHtml;
					noteToolEle = $('<div class="note-tool" style="display:none;">' + html + '</div>');
					$("body").append(noteToolEle);
					noteToolEle.find('.note-tool-item').each(function(i, n){
						var tooEle = $(n);
						
						tooEle.off('click.touchnote').on('click.touchnote', function(event){
							event.stopPropagation();
							var selection = ele.data('selection'); //data中取
							// 内置了划线、备注、复制3种方法，需要样式对应，其他的可以通过重写tooltopsHtml和tooltopsClickOthers方法实现
							if(tooEle.hasClass('icon-line') || tooEle.find('.icon-line').length){ // 划线								
								options.tooltopsClickLine(tooEle, root, ele, selection, options);
							} else if(tooEle.hasClass('icon-note') || tooEle.find('.icon-note').length){ //备注															
								options.tooltopsClickNote(tooEle, root, ele, selection, options);								
							} else if(tooEle.hasClass('icon-copy') || tooEle.find('.icon-copy').length){ //复制
								options.tooltopsClickCopy(tooEle, root, ele, selection, options);
							} else {
								options.tooltopsClickOthers(tooEle, root, ele, selection, options);
							}
							
							noteToolEle.hide(); //点击完成后隐藏							
						});
					});
				}
				ele.data('selection', selection); //放置到data中后面再取
				noteToolEle.removeClass('up').addClass('down'); //移除，重新定位
				noteToolEle.show();
				
				var domRect = range.domRect;
				
				var scrollX = document.documentElement.scrollLeft || document.body.scrollLeft; //左右滚动条偏移量
				var scrollY = document.documentElement.scrollTop || document.body.scrollTop; //上下滚动条偏移量
				var x = domRect.x + scrollX + domRect.width/2 ; // 距窗口左侧距离 + 滚动条偏移 + 选区宽度的一半 - 提示栏的宽度一半
				var y = domRect.y + scrollY + domRect.height + 10;
				
				ele.data('dialog-x', x);
				
				x -= noteToolEle.width()/2; // 距窗口左侧距离 + 滚动条偏移 + 选区宽度的一半 - 提示栏的宽度一半
				
				var height = $(window).height()+scrollY;
				if(y + noteToolEle.height() > height){ //下方显示不开，显示到上方
					y = domRect.y + scrollY -noteToolEle.height() - 10;
					noteToolEle.removeClass('down').addClass('up');
				}
				ele.data('dialog-y', y);
				
				noteToolEle.offset({left:x, top:y});
			}
			
			// 获取选中的节点
			var nodes = selection.selectedNodes;
		
			// 临时高亮处理		
			warapTmpHighlight(nodes, options.hltmpClsName, "tmp-tn-"+new Date().getTime());
			return false;
		} else if (noteToolEle.length) { // 未选中则隐藏提示栏				
			noteToolEle.hide();			
		}
	}
	// 鼠标划过的处理函数
	function mouseOver(event, root, ele, options) {		
		var target = event.target;
		if(options.mouseOverBefore(event, root, ele, options) && touchNote.isHighlightWrapNode(target)){
			//在高亮节点
			var targetEle = $(target);
			options.mouseOverInHigh(targetEle, event, root, ele, options);
			
		}
		options.mouseOverAfter(event, root, ele, options);
		
	}
	// 鼠标划过的前置处理，如果返回false则不继续执行了
	function mouseOverBefore(event, root, ele, options) {
		//var noteClassName = options.hlClsName; // 批注样式
		//$("."+noteClassName).removeClass("selected");
		return true;
	}
	// mouseover事件结束方法
	function mouseOverAfter(event, root, ele, options) {
		// 待处理
	}
	// 鼠标划过时再高亮区的处理
	function mouseOverInHigh(targetEle, event, root, ele, options) {
		// 待处理
	}
	// 窗口的竖向是否有滚动条
	function hasScrollbar() {
		return document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight);
	}
	// 向ele中添加属性
	function addDatas(ele, data){
		ele.attr('data-nid', data.nid);
		ele.attr('data-remarks', data.remarks); //备注内容
		ele.attr('data-keycode', JSON.stringify(data.keycode)); //对象转字符串，用data取时是对象				
		ele.attr('data-keyid', data.keyid);
		
		ele.attr('data-userid', data.userid);
		ele.attr('data-username', data.username);
		ele.attr('data-createtime', data.createtime);
	}
	// 监听滚动事件
	var lastScrolly=0;
	
	setTimeout(function(){
		// 触发滚动，以便刷新时显示在正常位置
		$(window).triggerHandler('scroll.touchnote');	
	}, 500); //延时	
	
	// 扩展对外属性
	$.extend($.fn.touchNote, {
		// 触发的方法
		triggerMethod : ["blur"],//,"keyup"
		// 默认参数
		defaults : {
			/** 根对象 */
			root: document.body, // document.body;
			/** 用户名，静态演示用，动态数据一般不需要从session或cookie中获取 */
			username:'兔先生',
			/** 用户头像，静态演示用，动态数据一般不需要从session或cookie中获取 */
			userimage:'',
			/** 父节点选择器，只有符合选择器的才作为父节点，用于计算文本的相对偏移量，尽量使用固定不变的节点选择器减少内容变动引起的节点变化，如段落或内容的顶级容器 */
			parSelectors:'',
			/** 备注容器jquery选择器 */
			noteContainerSelector : '.notesshow_container_div',
			/** 备注数量选择器 */
			noteCountSelector : '.notesshow_container_div .notesCount',
			/** 备注条目样式名称 */
			noteItemClsName : 'notesdiv',
			/** 备注条目选中样式名称 */
			noteSelectedClsName : 'noteselected',
			/** 高亮包裹标签 */
			hltagName : 'span',
			/** 临时高亮样式名 */
			hltmpClsName : "tmp_touch_notes",
			/** 高亮样式名 */
			hlClsName : "touch_notes",
			/** 高亮初始化样式名 */
			hlIntClsName : "nodetips-init",			
			/** 高亮数据，数据字段{'id':id, 'keycode':keycode, 'remarks':content, 'username':username, 'createtime':createtime} */
			hldatas : [],
			/** 元素高亮后的处理方法，参数(ele, data, tipsClass, tipsContentClass, options) */
			highlightNoteAfter:highlightNoteAfter,
			/** mouseup事件方法 */
			mouseup : mouseUp,
			/** mouseover事件方法 */
			mouseover : mouseOver,
			/** mouseover事件开始处理事件，返回boolean,为false则不往下进行,参数(event, root, ele, options)，如果重定义了mouseover则不调用 */
			mouseOverBefore:mouseOverBefore,
			/** mouseover事件在高亮节点的处理事件,(targetEle, event, root, ele, options)，如果重定义了mouseover则不调用 */
			mouseOverInHigh:mouseOverInHigh,
			/** mouseover事件结束方法,参数(event, root, ele, options)，如果重定义了mouseover则不调用 */
			mouseOverAfter:mouseOverAfter,
			/** 高亮节点的鼠标划过事件，数组，第一个function为鼠标移入事件，第二个为移出事件，分别为增加/移出selected样式 */
			highlightNoteMouseover:[function(event, root, ele, options){
				var noteClassName = options.hlClsName; // 批注样式
				$("."+noteClassName).removeClass("selected");
				
				var highlightId = ele.data(touchNote.DATA_ID);				
				
				var codeNodeEles = $('.'+noteClassName+"[data-"+touchNote.DATA_ID+"='"+highlightId+"']");
				codeNodeEles.addClass('selected'); //当前选中高亮
				// 选区重叠时，重叠的也显示
				// 扩展，这个判断存在问题，如果DATA_ID里面的值比较短，可能存在不相等也包含的情况，如11和1,12和1等				
				var codeNodeExtraEles = $('.'+noteClassName+"[data-"+touchNote.DATA_ID_EXTRA+"*='"+highlightId+"']");
				codeNodeExtraEles.addClass('selected');
				
			},function(event, root, ele, options){
				var noteClassName = options.hlClsName; // 批注样式
				$("."+noteClassName).removeClass("selected");
			}],
			/** 弹出框html，提交按钮为submitbtn，删除按钮为deletebtn */
			dialogHtml:'<div class="edit-textarea-wrap"><textarea class="edit-textarea"></textarea><button class="submitbtn">提交</button><button class="deletebtn">删除</button></div>',
			/** 打开弹出框， */
			openDialog:function(invokerEle, keytype, url, params, title, containerE, options){				
				
				var dialogEle = $(".note-dialog");
				// 创建弹出框
				if (dialogEle.length==0) {					
					var html = options.dialogHtml;
					dialogEle = $('<div class="note-dialog down" style="top:20px;">' + html + '</div>');
					$("body").append(dialogEle);					
				}
							
				var x = containerE.data('dialog-x'); //选中区域时记录			
				var y = containerE.data('dialog-y');
				
				dialogEle.find("textarea.edit-textarea").val(''); //清空输入框
				
				// 控制按钮显示
				if(keytype == touchNote.OPERATE_Type.UPDATE){ //修改		
					dialogEle.find(".deletebtn").show(); // 显示删除按钮
					// var e = event || window.event;
					x = invokerEle.offset().left + invokerEle.outerWidth(true)/2;				
					y = invokerEle.offset().top + invokerEle.outerHeight(true) + 8;
					
					dialogEle.find("textarea.edit-textarea").val(invokerEle.data('remarks'));
					
				} else if(keytype == touchNote.OPERATE_Type.ADD){ //添加
					dialogEle.find(".deletebtn").hide(); // 隐藏删除按钮
				}
				dialogEle.data('keytype', keytype);
				dialogEle.data('invoker', invokerEle);
				
				var initdata = dialogEle.data('inidate'); //初始化数据，一次即可
				if(!initdata){
					dialogEle.data('inidate', true);
					dialogEle.off('click.touchnote').on('click.touchnote', function(){
					
						return false; //防止冒泡，点击弹出框内内容时被关闭
					});
					// 保存按钮
					dialogEle.find('.submitbtn').off('click.touchnote').on('click.touchnote', function(){
						var remarks = dialogEle.find("textarea.edit-textarea").val();
						var keytypev = dialogEle.data('keytype');
						if(keytypev == touchNote.OPERATE_Type.UPDATE){ //修改
							// 修改当前元素的值
							var invokerEle = dialogEle.data('invoker');
							// 更新备注
							invokerEle.attr('data-remarks', remarks);
							invokerEle.data('remarks', remarks);
							// 将备注区内容修改了，生产环境中是与后台交互
							var keyid = params.keyid; // 获取keyid						
							// 获取备注区并更新备注区内容
							if (keyid) {
								var noteItemClsName = containerE.getOptions('noteItemClsName'); // 获取对应的备注区
								$("."+noteItemClsName+"[data-keyid='"+keyid+"']").find('.remarks').text(remarks); //点击
							}						
							
						} else if(keytypev == touchNote.OPERATE_Type.ADD){ //添加
							var nodes = containerE.addHighlighter(remarks);
							console.log(nodes);
						}
						
						// 提交数据，需要自己实现,返回值为true关闭
						if(options.submitData(remarks, keytypev, params, invokerEle, dialogEle, options)){
							dialogEle.find("textarea.edit-textarea").val('');
							dialogEle.hide(); //隐藏弹出框
						}
					});
					
					
					// 删除按钮
					dialogEle.find('.deletebtn').off('click.touchnote').on('click.touchnote', function(){
						// 删除高亮
						var invokerEle = dialogEle.data('invoker');
						touchNote.removeHighlight(containerE, invokerEle.data(touchNote.CAMEL_DATA_ID));
						dialogEle.find("textarea.edit-textarea").val('');
						dialogEle.hide(); //隐藏弹出框					
						
					});
				}
				console.log(x+" "+ y+" "+dialogEle.outerWidth(true)/2)
				console.log(x- dialogEle.outerWidth(true)/2)
				
				
				// 如果弹出框比窗口还大，那就缩放到窗口大小
				var winWidth = $(window).width();
				if (dialogEle.width()>winWidth) {
					dialogEle.width(winWidth-8);
				}
				var diaWidth = dialogEle.outerWidth(true);
				var leftv = x- diaWidth/2;
				// 左边距离不能小于0
				if(leftv<4){
					leftv = 5;
				}
				var leftv = x- diaWidth/2;
				// 右边距离不能小于0
				if(leftv+diaWidth > winWidth){
					leftv = winWidth-diaWidth-2;
				}
				dialogEle.show().offset({left:leftv, top:y});
				
			},
			/** 数据提交，提交成功返回true，失败返回false，remarks为备注， */
			submitData:function(remarks, keytype, params, ele, dialogEle, options){
				console.log("数据提交后执行，如保存数据库等, 正式环境下建议覆盖", remarks, keytype, params);
				return true;
			},
			/** 提示栏html，里面的条目必须用含有note-tool-item的样式，如果使用内置的划线、备注、复制方法则样式必须对应icon-line、icon-note、icon-copy并且包裹在note-tool-item中 */
			tooltopsHtml:'<div class="note-tool-item"><i class="icon-line">划线</i></div><div class="note-tool-item"><i class="icon-note">备注</i></div><div class="note-tool-item"><i class="icon-copy">复制</i></div>',
			/** 提示栏，划线的处理方法 */
			tooltopsClickLine:function(tooEle, root, ele, selection, options){
				console.log("划线，暂未实现，可自行扩展");
			},
			/** 提示栏，备注的处理方法，ele为容器jquery对象 */
			tooltopsClickNote:function(tooEle, root, ele, selection, options){
				console.log("备注");
				var urlAdd = ele.data("url-add"); // 添加的url								
				
				console.log(selection)
				var text = selection.text;
				var htmltext = selection.text;
				//var serializeStr = touchNote.serializeSelection(root, selection, options.parSelectors);
				var hs = touchNote.getHighlightSource(root, selection, options.parSelectors);			
				var serializeStr = touchNote.serialize2Str(hs);
				
				var params = {keycode:serializeStr, 
					cvid:ele.data('cvid'), 
					keyid:hs.id,
					startTag:hs.startMeta.parentTagName,
					startTagIndex:hs.startMeta.parentIndex,
					startTextOffset:hs.startMeta.textOffset,
					endTag:hs.endMeta.parentTagName,
					endTagIndex:hs.endMeta.parentIndex,
					endTextOffset:hs.endMeta.textOffset,
					text:text, 
					htmltext:htmltext};				
				
				options.openDialog(tooEle, touchNote.OPERATE_Type.ADD, urlAdd, params, '添加备注', ele, options);
			},
			/** 提示栏，复制的处理方法 */
			tooltopsClickCopy:function(tooEle, root, ele, selection, options){
				console.log("复制，暂未实现，可自行扩展");
			},
			/** 提示栏，除了划线，备注，复制外，其他的事件处理方法，结合tooltopsHtml自定义实现 */
			tooltopsClickOthers:function(tooEle, root, ele, selection, options){
				console.log("提示栏的其他方法，未实现……");
			},
			/** 添加备注区html内容，有需要时可以覆盖,索引的class为number，用户名class为username，文本class为text，备注class为remarks，创建时间class为createtime。  参数(data, index, noteContainerEle, root, options, datas)，data为数据，index为下标,datas为所有数据，noteContainerEle为容器jquery对象，root为根元素，ele为处理的对象,options为配置*/
			addNoteHtml : function(data, index, datas, noteContainerEle, root, ele, options){
				
				var html = '<div class="'+options.noteItemClsName+' panel panel-default" title="点击右键可以对该条备注进行修改、删除">'+
							'	<div class="panel-heading">'+
							'		<span class="catalog_number_wraper"><span class="catalog_number fs12 number">'+(index+1)+'</span></span>'+
							'		<h6 class="panel-title fs12 username" style="display: inline-block;">'+data.username+'</h6>'+
							'		<label class="fr menus">…</label>'+
							'	</div>'+
							'	<div class="bs-callout bs-callout-danger text">'+data.keycode.text+'</div>'+
							'	<div class="panel-body fs12 pt5"><div class="fs11 createtime">'+data.createtime+'</div><div class="remarks">'+data.remarks+'		</div></div>'+
							'</div>';
				return html;
				
			},
			/** 添加备注区html内容，有需要时可以覆盖。  参数(data, index, noteContainerEle, root, options, datas)，data为数据，index为下标,datas为所有数据，noteContainerEle为容器jquery对象，root为根元素，ele为处理的对象,options为配置*/
			getAddNoteEle : function(data, index, datas, noteContainerEle, root, ele, options){
				
				// 生成html
				var html = options.addNoteHtml(data, index, datas, noteContainerEle, root, ele, options );
				var noteEle = $(html);
				// 可能存在特殊字符转义,重写赋值
				noteEle.find('.text').text(data.keycode.text); 
				noteEle.find('.remarks').text(data.remarks); 
				return noteEle;
			},
			fixNoteTopFn : function(noteContainerEle, root, ele, options){
				var notesContainerDiv = $(options.noteContainerSelector);
				var offset = notesContainerDiv.data('ori_offset');
				if(!offset){
					offset = notesContainerDiv.offset();
					notesContainerDiv.data('ori_offset', offset);
				}
				var offsetOldVal = 0;
				if(offset){
					offsetOldVal = offset.top;
				}				
				return offsetOldVal;
			},
			/** 右侧备注栏操作html, touch-note-popmenus必须有，里面的条目必须用含有note-tool-item的样式，，修改添加样式touchnote-update，删除添加样式touchnote-delete */
			noteOperateHtml : '<div class="touch-note-popmenus down"><div class="note-tool-item"><i class="touchnote-update">修改</i></div><div class="note-tool-item"><i class="touchnote-delete">删除</i></div></div>',
			/** 添加备注区元素事件 参数(noteEle, noteContainerEle, root, options, datas)，noteEle为备注区jquery元素，noteContainerEle为容器jquery对象，root为根元素，options为配置*/
			addNoteEleFn : function(noteEle, noteContainerEle, root, ele, options){
				var url = ele.data("url-upd"); // 添加的url							
				noteEle.on("update", function(){ //修改
				
					var datas = noteEle.data();
					var params = {keycode:JSON.stringify(datas.keycode), 
							keyid:datas.keycode.id,
							startTag:datas.keycode.startMeta.parentTagName,
							startTagIndex:datas.keycode.startMeta.parentIndex,
							startTextOffset:datas.keycode.startMeta.textOffset,
							endTag:datas.keycode.endMeta.parentTagName,
							endTagIndex:datas.keycode.endMeta.parentIndex,
							endTextOffset:datas.keycode.endMeta.textOffset,
							text:datas.keycode.text, 
							htmltext:datas.keycode.text,
							remarks:datas.remarks};
					// 打开弹出框
					options.openDialog(noteEle, touchNote.OPERATE_Type.UPDATE, url, params, '修改备注', ele, options);
					
				}).on("delete", function(){ //删除
					
					// 可能需要需要后台交互，请重写
					var params = {id:noteEle.data("nid")};				
					
					//var nodes = noteEle.triggerHandler("highlightele"); //获取对应的高亮元素
					//nodes.removeClass(codeNoteClassName+" touchjs-tips "+hlIntClsName).removeData("target").removeAttr("data-target").off(".touchjs_tips");
					
					touchNote.removeHighlight(ele, noteEle.data("keycode").id);				
					
					noteEle.remove();
					// 更新备注总数 
					noteContainerEle.triggerHandler("refreshCount");
						
				}).on("showInVisible", function(){ // 显示到可视区
					var $this = noteEle;
					var notesContainerDiv = $(options.noteContainerSelector);
					var scrollY = document.documentElement.scrollTop || document.body.scrollTop; //上下滚动条偏移量,即上部遮挡内容的高度
					var height = winHeight+scrollY;
					var extendHeight = options.fixNoteTopFn(noteContainerEle, root, ele, options);
					//if(!scrollY){ //如果没有滚动条，则到原来的高度就可以了，不需要到最顶部,顶部存在固定元素所以距顶部高度时固定的
						//extendHeight = offsetOldVal;
					//}
					if($this.height() < winHeight){ //未超过1屏，则全部显示到可视区			
						var eleHeight = $this.offset().top + $this.height();
						if(eleHeight > height || $this.offset().top < scrollY){ //可视区下方 || 可视区上方
							notesContainerDiv.offset({top : scrollY - ($this.offset().top - notesContainerDiv.offset().top) + extendHeight})
						}
					} else { // 超过1屏
						if($this.offset().top > height || $this.offset().top < scrollY){ //可视区下方 || 可视区上方							
							notesContainerDiv.offset({top : scrollY - ($this.offset().top - notesContainerDiv.offset().top) + extendHeight})
						}
					}
				}).on("highlightele", function(){ //获取对应的高亮元素
					var $this = noteEle;
					var dataid = $this.data('keyid');
					var keycode = $this.data("keycode");
					var keyid= keycode.id;
					
					var hlClsName = ele.getOptions('hlClsName'); //高亮样式
					
					// 不通过nid，通过data-highlight-id查找					
					var nodes = ele.find("."+hlClsName+"[data-"+touchNote.DATA_ID+"='"+dataid+"']");
					
					// 合并重叠部分
					nodes = nodes.add(ele.find("."+hlClsName+"[data-highlight-id-extra*='"+keyid+"']"));
					return nodes;
				}).on("click", function(){ //点击则内容区高亮，并且拉到可视范围内
				
					var fixedHeight = options.fixNoteTopFn(noteContainerEle, root, ele, options);
					var hlClsName = ele.getOptions('hlClsName'); //高亮样式
					var $this = noteEle;
					// 选中样式处理
					$('.'+options.noteItemClsName).removeClass(options.noteSelectedClsName); 
					$this.addClass(options.noteSelectedClsName);			
					
					// 获取高亮节点
					var nodes = $this.triggerHandler("highlightele");
					
					// 先移除高亮，再添加高亮
					ele.find("."+hlClsName).removeClass('selected');
					nodes.addClass('selected');
					
					if(nodes.length){
						var nodeFirst = nodes.first(); //取第一个元素
						// 如果不再可视区内，则滚动到可视区
						var scrollY = document.documentElement.scrollTop || document.body.scrollTop; //上下滚动条偏移量,即上部遮挡内容的高度
						var height = winHeight+scrollY;
						
						// offset是不会随着滚动条的变化而变化的
						if (hasScrollbar() && (nodeFirst.offset().top > height || nodeFirst.offset().top < scrollY+fixedHeight)) { //y轴出现滚动条且超出可视范围则滚动
							//滚动到距窗口顶部50px的位置
							$("html,body").animate({scrollTop:(nodeFirst.offset().top - 80)+ "px"}, 800,function(){
								// 该元素也需要在可视区,如果不在，拉倒可视区内							
								$this.triggerHandler("showInVisible");						
							}); 
							
						} else {					
							$this.triggerHandler("showInVisible");
						}
						
					}
					
				});
				
				// 操作方法
				noteEle.find(".menus").on('click', function(){
					var menusE = $(this);						
					
					var popEles = $('.touch-note-popmenus');
					if(!popEles.length){
						var popHtml = options.noteOperateHtml;
						popEles = $(popHtml);
						$("body").append(popEles);
					}
					// 显示，并调整显示位置
					popEles.show().offset({left:menusE.offset().left+menusE.width()-popEles.width()+13, top:menusE.offset().top+menusE.outerHeight(true)});
					
					popEles.find('.note-tool-item').each(function(i, n){
						var itemEle = $(n);
						
						itemEle.off('click.touchnote').on('click.touchnote', function(){
							// 内置了修改、删除2种方法，需要样式对应，其他的可以通过重写tooltopsHtml和tooltopsClickOthers方法实现
							if(itemEle.hasClass('touchnote-update') || itemEle.find('.touchnote-update').length){ // 修改						
								options.noteUpdateFn(noteEle, itemEle, root, options);
							} else if(itemEle.hasClass('touchnote-delete') || itemEle.find('.touchnote-delete').length){ // 删除								
								options.noteDeleteFn(noteEle, itemEle, root, options);								
							} else {
								options.noteClickOtherFn(noteEle, itemEle, root, options);
							}
						});
					});
					
					return false;
				});
				
				var initdocclick = noteContainerEle.data("initdocclick");
				if(!initdocclick){
					noteContainerEle.data("initdocclick", true);
					// 取消无效的高亮显示
					$(document).off("click.note.touchnote").on("click.note.touchnote", function(){
						$('.touch-note-popmenus').hide();
					});
				}				
				
			},	
			/** 备注更新 参数(noteEle, itemEle, root, options)，noteEle为备注对象，itemEle为操作条目对象，root为根元素，options为配置*/
			noteUpdateFn : function(noteEle, itemEle, root, ele, options){
				noteEle.triggerHandler("update");
			},
			/** 备注删除 参数(noteEle, itemEle, root, options)，noteEle为备注对象，itemEle为操作条目对象，root为根元素，options为配置*/
			noteDeleteFn : function(noteEle, itemEle, root, ele, options){
				noteEle.triggerHandler("delete");
			},
			/** 备注其他方法 参数(noteEle, itemEle, root, options)，noteEle为备注对象，itemEle为操作条目对象，root为根元素，options为配置*/
			noteClickOtherFn : function(noteEle, itemEle, root, ele, options){
				console.log("划线，暂未实现，可自行扩展");
			},			
			/** 添加备注区内容 参数(data, index, noteContainerEle, root, options, datas)，data为数据，index为下标,datas为所有数据，noteContainerEle为容器jquery对象，root为根元素，options为配置*/
			addNoteEle : function(data, index, datas, noteContainerEle, root, ele, options){
				// 生成html对应的jquery对象
				var noteEle = null;
				if(noteContainerEle.length){
					noteEle = options.getAddNoteEle(data, index, datas, noteContainerEle, root, ele, options );
				
					// 添加属性
					addDatas(noteEle, data);
					// 添加元素事件
					options.addNoteEleFn(noteEle, noteContainerEle, root, ele, options );
					
					noteContainerEle.append(noteEle);
					
					var scrollbind = noteContainerEle.data('scrollbind');
					if(!scrollbind){
						noteContainerEle.data('scrollbind', true);
						
						var offset = noteContainerEle.offset();
						noteContainerEle.data('ori_offset', offset);
						noteContainerEle.data('ori_position', noteContainerEle.css('position'));
						noteContainerEle.data('ori_width', noteContainerEle.width());
						
						var fixedHeight = options.fixNoteTopFn(noteContainerEle, root, ele, options);
						// 暂时不支持多个
						$(window).off("scroll.note.touchnote").on("scroll.note.touchnote", function(e){
							var scrollY = document.documentElement.scrollTop || document.body.scrollTop; //上下滚动条偏移量,即上部遮挡内容的高度
							// 需要保证备注内容一直在可视区域内，如果超过一屏则居底显示最后几个，如果不超过1屏则居上显示全部，如果没有滚动条了则回复原始值
							if(scrollY){ //出现滚动条，计算顶部距离
							console.log(noteContainerEle.height()+" "+ winHeight)
								if(noteContainerEle.height()> winHeight){ //大于1屏，超出窗口高度才处理，否则不处理
									if(noteContainerEle.height() + noteContainerEle.offset().top < scrollY + winHeight){ // 底部要留白了
										noteContainerEle.offset({top : scrollY - (noteContainerEle.height() - winHeight) -20 });
									}
									if (lastScrolly>scrollY && noteContainerEle.offset().top > scrollY) { //向上滚动
									
										noteContainerEle.offset({top : scrollY + fixedHeight});
									}
								} else { //小于1屏，50是因为又顶部固定的元素
									noteContainerEle.css({'position' : 'fixed', 'top':fixedHeight, 'left':noteContainerEle.data('ori_offset').left}).width(noteContainerEle.data('ori_width'));
								}						
							} else {
								// 还原
								noteContainerEle.css('position', noteContainerEle.data('ori_position'));
								noteContainerEle.offset(noteContainerEle.data('ori_offset'));
							}		
							lastScrolly = scrollY;
							// 隐藏右侧操作栏
							$('.touch-note-popmenus').hide();
						});
					}
				}
				
				return noteEle;
				
			},
			/** 更新备注总数 参数(count, datas, root, options)，count为总数，datas为高亮数据，root为根元素，options为配置 */
			updateNotesCount : function(count, datas, root, ele, options){
				$(options.noteCountSelector).text(count);
			},
			/** 刷新备注区总数 参数(count, datas, root, options)，count为总数，datas为高亮数据，root为根元素，options为配置 */
			refreshNotesCount : function(noteContainerEle, root, ele, options){
				// 更新备注总数 count, datas, root, options
				options.updateNotesCount(noteContainerEle.find("."+ele.getOptions('noteItemClsName')).length, null, root, ele, options);
				// 更新数目
				noteContainerEle.find("."+ele.getOptions('noteItemClsName')).each(function(i, n){
					console.log(i);
					console.log(n);
					$(n).find(".number").text(i+1);
				});
			}
		}
	});
	
})(jQuery);
